import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import serial
import serial.tools.list_ports
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.colors import LinearSegmentedColormap
import threading
import time
import queue
import csv
from datetime import datetime
import struct

plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题


class ThermalImagingApp:
    def __init__(self, root):
        self.root = root
        self.root.title("32x32 热成像查看器")
        self.root.geometry("900x800")
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

        # 串口相关变量
        self.serial_port = None
        self.serial_queue = queue.Queue()
        self.receiving_data = False
        self.pixel_buffer = []
        self.current_row = 0
        self.data_count = 0

        # 自动更新控制
        self.auto_update = False
        self.update_interval = 50  # 毫秒

        # 创建自定义热图颜色映射 (蓝-青-黄-红)
        self.colors = [
            (0, 0, 1),  # 蓝色
            (0, 1, 1),  # 青色
            (1, 1, 0),  # 黄色
            (1, 0, 0)  # 红色
        ]
        self.cmap = LinearSegmentedColormap.from_list("thermal_cmap", self.colors, N=256)

        # 存储当前温度数据
        self.current_image_data = None
        self.max_temp_marker = None
        self.min_temp_marker = None
        self.center_temp_marker = None
        self.click_marker = None
        self.click_text = None
        self.threshold_line = None

        # 阈值设置
        self.threshold_var = tk.DoubleVar(value=35.0)
        self.highlight_threshold = False

        # 创建UI
        self.create_widgets()

        # 启动串口数据轮询线程
        self.serial_thread = threading.Thread(target=self.poll_serial, daemon=True)
        self.serial_thread.start()

        self.last_send_time = None  # 添加时间记录变量

        # 启动GUI更新定时器
        self.update_gui()

    def create_widgets(self):
        # 串口控制框架
        control_frame = ttk.LabelFrame(self.root, text="串口控制")
        control_frame.pack(fill=tk.X, padx=10, pady=5)

        # 串口选择
        ttk.Label(control_frame, text="串口:").grid(row=0, column=0, padx=5, pady=5)
        self.port_combo = ttk.Combobox(control_frame, width=15)
        self.port_combo.grid(row=0, column=1, padx=5, pady=5)

        # 刷新按钮
        self.refresh_btn = ttk.Button(control_frame, text="刷新端口", command=self.refresh_ports)
        self.refresh_btn.grid(row=0, column=2, padx=5, pady=5)

        # 连接按钮
        self.connect_btn = ttk.Button(control_frame, text="连接", command=self.toggle_connection)
        self.connect_btn.grid(row=0, column=3, padx=5, pady=5)

        # 捕获按钮
        self.capture_btn = ttk.Button(control_frame, text="捕获图像 (a)", command=self.request_image, state=tk.DISABLED)
        self.capture_btn.grid(row=0, column=4, padx=5, pady=5)

        # 自动更新控制
        self.auto_update_var = tk.BooleanVar(value=False)
        self.auto_update_btn = ttk.Checkbutton(
            control_frame, text="自动更新",
            variable=self.auto_update_var,
            command=self.toggle_auto_update,
            state=tk.DISABLED
        )
        self.auto_update_btn.grid(row=0, column=5, padx=5, pady=5)

        # 状态标签
        self.status_var = tk.StringVar(value="就绪")
        ttk.Label(control_frame, textvariable=self.status_var).grid(row=0, column=6, padx=10, pady=5)

        # 热成像显示框架
        display_frame = ttk.LabelFrame(self.root, text="热成像图像")
        display_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)

        # 创建Matplotlib图形
        self.fig, self.ax = plt.subplots(figsize=(7, 6))
        self.ax.set_title("热成像数据 (℃)")
        self.ax.set_xticks([])
        self.ax.set_yticks([])

        # 初始空白图像
        self.blank_data = np.zeros((32, 32))
        self.img = self.ax.imshow(self.blank_data, cmap=self.cmap, interpolation='bilinear')
        self.cbar = self.fig.colorbar(self.img, ax=self.ax, label='温度 (℃)')

        # 将图形嵌入Tkinter
        self.canvas = FigureCanvasTkAgg(self.fig, master=display_frame)
        self.canvas.draw()
        self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        # 绑定点击事件
        self.canvas.mpl_connect('button_press_event', self.on_image_click)

        # 创建工具栏
        toolbar_frame = tk.Frame(display_frame)
        toolbar_frame.pack(fill=tk.X, padx=5, pady=5)

        # 保存按钮
        self.save_btn = ttk.Button(
            toolbar_frame, text="保存图像",
            command=self.save_image, state=tk.DISABLED
        )
        self.save_btn.pack(side=tk.LEFT, padx=5)

        # 保存数据按钮
        self.save_data_btn = ttk.Button(
            toolbar_frame, text="保存数据",
            command=self.save_data, state=tk.DISABLED
        )
        self.save_data_btn.pack(side=tk.LEFT, padx=5)

        # 阈值控制
        threshold_frame = tk.Frame(toolbar_frame)
        threshold_frame.pack(side=tk.RIGHT, padx=10)

        ttk.Label(threshold_frame, text="阈值(℃):").pack(side=tk.LEFT)
        self.threshold_entry = ttk.Entry(
            threshold_frame, textvariable=self.threshold_var, width=5
        )
        self.threshold_entry.pack(side=tk.LEFT, padx=2)

        self.threshold_btn = ttk.Button(
            threshold_frame, text="应用阈值",
            command=self.apply_threshold, state=tk.DISABLED
        )
        self.threshold_btn.pack(side=tk.LEFT, padx=5)

        # 数据信息框架
        info_frame = ttk.Frame(self.root)
        info_frame.pack(fill=tk.X, padx=10, pady=5)

        ttk.Label(info_frame, text="最小值 (℃):").grid(row=0, column=0, padx=5)
        self.min_var = tk.StringVar(value="0.00")
        ttk.Label(info_frame, textvariable=self.min_var).grid(row=0, column=1, padx=5)

        ttk.Label(info_frame, text="最大值 (℃):").grid(row=0, column=2, padx=5)
        self.max_var = tk.StringVar(value="0.00")
        ttk.Label(info_frame, textvariable=self.max_var).grid(row=0, column=3, padx=5)

        ttk.Label(info_frame, text="平均值 (℃):").grid(row=0, column=4, padx=5)
        self.avg_var = tk.StringVar(value="0.00")
        ttk.Label(info_frame, textvariable=self.avg_var).grid(row=0, column=5, padx=5)

        ttk.Label(info_frame, text="中心温度 (℃):").grid(row=0, column=6, padx=5)
        self.center_var = tk.StringVar(value="0.00")
        ttk.Label(info_frame, textvariable=self.center_var).grid(row=0, column=7, padx=5)

        ttk.Label(info_frame, text="点击温度 (℃):").grid(row=0, column=8, padx=5)
        self.click_temp_var = tk.StringVar(value="-")
        ttk.Label(info_frame, textvariable=self.click_temp_var).grid(row=0, column=9, padx=5)

        # 刷新串口列表
        self.refresh_ports()

    def refresh_ports(self):
        ports = serial.tools.list_ports.comports()
        port_names = [port.device for port in ports]
        self.port_combo['values'] = port_names
        if port_names:
            self.port_combo.current(0)
            self.status_var.set(f"发现 {len(ports)} 个串口")
        else:
            self.status_var.set("未找到串口")

    def toggle_connection(self):
        if self.serial_port and self.serial_port.is_open:
            self.close_serial()
        else:
            self.open_serial()

    def open_serial(self):
        port = self.port_combo.get()
        if not port:
            self.status_var.set("请选择串口!")
            return

        try:
            self.serial_port = serial.Serial(
                port=port,
                baudrate=921600,
                timeout=0.1
            )
            self.connect_btn.config(text="断开连接")
            self.capture_btn.config(state=tk.NORMAL)
            self.auto_update_btn.config(state=tk.NORMAL)
            self.save_btn.config(state=tk.NORMAL)
            self.save_data_btn.config(state=tk.NORMAL)
            self.threshold_btn.config(state=tk.NORMAL)
            self.status_var.set(f"已连接: {port}")
        except Exception as e:
            self.status_var.set(f"错误: {str(e)}")
            messagebox.showerror("连接错误", f"无法连接串口:\n{str(e)}")

    def close_serial(self):
        if self.serial_port and self.serial_port.is_open:
            self.serial_port.close()
        self.connect_btn.config(text="连接")
        self.capture_btn.config(state=tk.DISABLED)
        self.auto_update_btn.config(state=tk.DISABLED)
        self.auto_update_var.set(False)
        self.auto_update = False
        self.save_btn.config(state=tk.DISABLED)
        self.save_data_btn.config(state=tk.DISABLED)
        self.threshold_btn.config(state=tk.DISABLED)
        self.status_var.set("已断开连接")
        self.receiving_data = True

    def toggle_auto_update(self):
        self.auto_update = self.auto_update_var.get()
        if self.auto_update:
            self.status_var.set("自动更新已启用")
            # 立即开始第一次更新
            self.request_image()
        else:
            self.status_var.set("自动更新已禁用")

    def request_image(self):
        if self.serial_port and self.serial_port.is_open and not self.receiving_data:
            self.serial_port.write(b'a')  # 发送触发字符
            # 计算并输出发送频率
            current_time = time.time()
            if self.last_send_time is not None:
                interval = current_time - self.last_send_time
                if interval > 0:
                    frequency = 1.0 / interval
                    print(f'发送频率: {frequency:.2f} Hz')
            else:
                print('首次发送')
            self.last_send_time = current_time
            self.status_var.set("请求已发送. 等待数据...")
            self.receiving_data = True
            self.pixel_buffer = []
            self.current_row = 0
            self.data_count = 0
            # 清除点击标记
            self.click_temp_var.set("-")
            if self.click_marker:
                self.click_marker.remove()
                self.click_marker = None
            if self.click_text:
                self.click_text.remove()
                self.click_text = None

    def poll_serial(self):
        frame_header = b'BEGIN'
        frame_size = 32 * 32 * 2  # BEGIN(5字节) + 32x32x2字节
        buffer = bytearray()

        while True:
            if self.serial_port and self.serial_port.is_open:
                try:
                    # 读取可用数据
                    data = self.serial_port.read(self.serial_port.in_waiting or 1)
                    if data:
                        buffer.extend(data)

                        # 帧处理逻辑
                        while True:
                            # 查找帧头
                            header_pos = buffer.find(frame_header)
                            if header_pos == -1:
                                break  # 没有找到帧头

                            # 检查是否收到完整帧
                            if len(buffer) - header_pos >= frame_size:
                                # 提取完整帧并放入队列
                                frame = buffer[header_pos + 5:header_pos + 5 + frame_size]
                                self.serial_queue.put(frame)

                                # 移除已处理数据
                                buffer = buffer[header_pos + frame_size:]
                            else:
                                break  # 等待更多数据
                except Exception as e:
                    self.status_var.set(f"串口错误: {str(e)}")
                    buffer = bytearray()  # 出错时清空缓冲区
                    time.sleep(0.1)

            time.sleep(0.01)

    def process_serial_data(self, data):
        if self.receiving_data:
            try:
                # 检查数据长度
                if len(data) != 2048:
                    self.status_var.set(f"数据长度错误: 收到{len(data)}字节，预期2048字节")
                    print(f"数据长度错误: 收到{len(data)}字节，预期2048字节")
                    self.process_complete_image()
                    return

                # 按大端序解析 short 数组
                raw_shorts = struct.unpack('<' + 'h' * 1024, data)  # 1024个short

                # 转换为摄氏度（不移除任何数据）
                raw_temperatures = [(int(x) / 10.0) - 273.15 for x in raw_shorts if x]

                # 确保转换后的数据长度正确
                if len(raw_temperatures) != 1024:
                    # self.status_var.set(f"温度数据长度异常: {len(raw_temperatures)}")
                    return

                # 清空缓冲区并填充32x32矩阵
                self.pixel_buffer = []
                for row in range(32):
                    start = row * 32
                    end = start + 32
                    self.pixel_buffer.append(raw_temperatures[start:end])  # 正确切片

                # 更新显示
                self.process_complete_image()

                # 重置计数器（如果需要）
                self.current_row = 0
                self.data_count = 0

            except struct.error as e:
                self.status_var.set(f"数据解析错误: {str(e)}")
            except Exception as e:
                self.status_var.set(f"处理错误: {str(e)}")


    def process_complete_image(self):
        if len(self.pixel_buffer) == 32:
            try:
                # 转换为numpy数组
                pixel_array = np.array(self.pixel_buffer, dtype=np.float32)
                pixel_array = np.flipud(pixel_array)
                self.current_image_data = pixel_array  # 保存当前温度数据

                # 更新热成像显示
                self.img.set_data(pixel_array)
                self.img.set_clim(vmin=np.min(pixel_array), vmax=np.max(pixel_array))

                # 更新温度标记
                self.update_temperature_markers(pixel_array)

                # 应用阈值高亮
                if self.highlight_threshold:
                    self.apply_threshold()

                self.canvas.draw()

                # 更新信息标签
                min_temp = np.min(pixel_array)
                max_temp = np.max(pixel_array)
                avg_temp = np.mean(pixel_array)

                self.min_var.set(f"{min_temp:.2f}")
                self.max_var.set(f"{max_temp:.2f}")
                self.avg_var.set(f"{avg_temp:.2f}")

                # 计算中心区域温度
                center_temp = np.mean(pixel_array[14:18, 14:18])
                self.center_var.set(f"{center_temp:.2f}")

                # self.status_var.set(
                #     f"图像已接收. 最小值: {min_temp:.2f}℃, 最大值: {max_temp:.2f}℃, 中心温度: {center_temp:.2f}℃"
                # )
            except Exception as e:
                self.status_var.set(f"处理错误: {str(e)}")
        else:
            self.status_var.set(f"接收的图像不完整! 收到 {len(self.pixel_buffer)}/32 行")

        self.receiving_data = False

        # 如果启用了自动更新，安排下一次请求
        if self.auto_update and not self.receiving_data:
            self.root.after(self.update_interval, self.request_image)

    def update_temperature_markers(self, pixel_array):
        # 清除旧标记和文本标签
        if self.max_temp_marker:
            self.max_temp_marker.remove()
            self.max_temp_marker = None
        if self.min_temp_marker:
            self.min_temp_marker.remove()
            self.min_temp_marker = None
        if self.center_temp_marker:
            self.center_temp_marker.remove()
            self.center_temp_marker = None

        # 清除所有文本标签
        if hasattr(self, 'max_text'):
            self.max_text.remove()
            delattr(self, 'max_text')
        if hasattr(self, 'min_text'):
            self.min_text.remove()
            delattr(self, 'min_text')
        if hasattr(self, 'center_text'):
            self.center_text.remove()
            delattr(self, 'center_text')

        # 标记最高温
        max_temp = np.max(pixel_array)
        max_idx = np.unravel_index(np.argmax(pixel_array), pixel_array.shape)
        self.max_temp_marker = self.ax.scatter(
            max_idx[1], max_idx[0], s=100,
            edgecolors='white', facecolors='none',
            linewidths=1.5
        )
        self.max_text = self.ax.text(
            max_idx[1], max_idx[0] + 1.5, f'MAX: {max_temp:.1f}℃',
            fontsize=15, color='black', ha='center', weight='bold',
            bbox=dict(facecolor='red', alpha=0.7, edgecolor='none', boxstyle='round,pad=0.5')
        )

        # 标记最低温
        min_temp = np.min(pixel_array)
        min_idx = np.unravel_index(np.argmin(pixel_array), pixel_array.shape)
        self.min_temp_marker = self.ax.scatter(
            min_idx[1], min_idx[0], s=100,
            edgecolors='white', facecolors='none',
            linewidths=1.5
        )
        self.min_text = self.ax.text(
            min_idx[1], min_idx[0] - 1.5, f'MIN: {min_temp:.1f}℃',
            fontsize=15, color='black', ha='center', weight='bold',
            bbox=dict(facecolor='blue', alpha=0.7, edgecolor='none', boxstyle='round,pad=0.5')
        )

        # 标记中心区域
        center_temp = np.mean(pixel_array[14:18, 14:18])
        self.center_temp_marker = self.ax.scatter(
            16, 16, s=80,
            edgecolors='white', facecolors='none',
            linewidths=1.2
        )
        self.center_text = self.ax.text(
            16, 19, f'中心: {center_temp:.1f}℃',
            fontsize=15, color='black', ha='center', weight='bold',
            bbox=dict(facecolor='white', alpha=0.7, edgecolor='none', boxstyle='round,pad=0.5')
        )

    def on_image_click(self, event):
        if event.inaxes == self.ax and self.current_image_data is not None:
            # 获取点击位置对应的像素坐标
            x = int(event.xdata)
            y = int(event.ydata)

            # 确保坐标在有效范围内
            if 0 <= x < 32 and 0 <= y < 32:
                # 获取温度值
                temp = self.current_image_data[y, x]
                self.click_temp_var.set(f"{temp:.2f}")

                # 清除旧标记
                if self.click_marker:
                    self.click_marker.remove()
                if self.click_text:
                    self.click_text.remove()

                # 创建新标记
                self.click_marker = self.ax.scatter(
                    x, y, s=80,
                    edgecolors='lime', facecolors='none',
                    linewidths=1.5
                )

                # 创建文本标签
                offset = 0.5 if y < 24 else -1.5  # 根据位置调整文本位置
                self.click_text = self.ax.text(
                    x, y + offset, f'({x},{y}): {temp:.1f}℃',
                    fontsize=9, color='lime', ha='center',
                    bbox=dict(facecolor='black', alpha=0.7, edgecolor='none')
                )

                self.canvas.draw()

    def apply_threshold(self):
        try:
            threshold = self.threshold_var.get()
            if self.current_image_data is not None:
                self.highlight_threshold = True

                # 清除旧阈值线
                if self.threshold_line:
                    self.threshold_line.remove()

                # 更新色条阈值
                self.threshold_line = self.cbar.ax.axhline(
                    y=threshold, color='white', linewidth=2, linestyle='--')

                self.cbar.ax.text(
                    0.5, threshold + 0.2, f'阈值: {threshold}℃',
                    color='white', ha='center',
                    transform=self.cbar.ax.get_yaxis_transform(),
                    bbox=dict(facecolor='red', alpha=0.5)
                )

                # 重新绘制
                self.canvas.draw()

                # 更新状态
                self.status_var.set(f"应用温度阈值: {threshold}℃")
        except Exception as e:
            self.status_var.set(f"阈值错误: {str(e)}")

    def save_image(self):
        if self.current_image_data is None:
            messagebox.showwarning("保存图像", "没有可用的图像数据")
            return

        file_path = filedialog.asksaveasfilename(
            defaultextension=".png",
            filetypes=[("PNG files", "*.png"), ("JPEG files", "*.jpg"), ("All files", "*.*")]
        )

        if file_path:
            try:
                # 创建临时图用于保存
                fig, ax = plt.subplots(figsize=(8, 7))
                ax.set_title(f"热成像图像 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")

                # 添加温度信息到标题
                info_title = (
                    f"最小值: {np.min(self.current_image_data):.1f}℃ | "
                    f"最大值: {np.max(self.current_image_data):.1f}℃ | "
                    f"中心: {np.mean(self.current_image_data[14:18, 14:18]):.1f}℃"
                )
                plt.figtext(0.5, 0.01, info_title, ha='center', fontsize=10)

                # 显示热图
                img = ax.imshow(self.current_image_data, cmap=self.cmap, interpolation='bilinear')
                fig.colorbar(img, ax=ax, label='温度 (℃)')

                # 保存图像
                fig.savefig(file_path, dpi=150, bbox_inches='tight')
                plt.close(fig)
                self.status_var.set(f"图像已保存至: {file_path}")
            except Exception as e:
                messagebox.showerror("保存错误", f"无法保存图像:\n{str(e)}")

    def save_data(self):
        if self.current_image_data is None:
            messagebox.showwarning("保存数据", "没有可用的温度数据")
            return

        file_path = filedialog.asksaveasfilename(
            defaultextension=".csv",
            filetypes=[("CSV files", "*.csv"), ("Text files", "*.txt"), ("All files", "*.*")]
        )

        if file_path:
            try:
                with open(file_path, 'w', newline='') as f:
                    writer = csv.writer(f)
                    # 写入标题
                    writer.writerow(['X', 'Y', '温度(℃)'])

                    # 写入数据
                    for y in range(32):
                        for x in range(32):
                            temp = self.current_image_data[y, x]
                            writer.writerow([x, y, f"{temp:.2f}"])

                self.status_var.set(f"温度数据已保存至: {file_path}")
            except Exception as e:
                messagebox.showerror("保存错误", f"无法保存数据:\n{str(e)}")

    def update_gui(self):
        # 处理串口队列中的所有消息
        while not self.serial_queue.empty():
            data = self.serial_queue.get()
            self.process_serial_data(data)

        # 每50更新一次GUI
        self.root.after(50, self.update_gui)

    def on_closing(self):
        if self.serial_port and self.serial_port.is_open:
            self.serial_port.close()
        self.root.destroy()


if __name__ == "__main__":
    root = tk.Tk()
    app = ThermalImagingApp(root)
    root.mainloop()
